# Copyright 2012 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""EasyFilter class is used for applying query filters to all types of classes.

This EasyFilter class is used to apply a filter to a single object for querying
purposes. For example, you can inherit this class from any other class to access
a 'Filter' method which grants SQL like querying functionality over a container
(such as a list or dict) of objects. You can think of the EasyFilter class as a
very minimal/lightweight implementation of Django's QuerySet API:

  http://docs.djangoproject.com/en/dev/ref/models/querysets/

There is also a bit of "LINQ" like functionality in EasyFilter:

  http://en.wikipedia.org/wiki/Language_Integrated_Query

Is most cases, you can just use list comprehensions to filter lists of objects.
However, sometimes the list comprehensions can get too large to fit on one line,
so, you're stuck writing a for loop with a bunch of boolean statements or
something similiar. You should use the EasyFilter's Filter method in cases
where the expression is too large to fit in a list comprehension and not unique
or complex enough to live in its own method or looping construct.


USAGE:
  See the unit tests for examples.

  1) Inherit the EasyFilter class in the class whose objects you want to filter.
  2) Create a list of objects with the derived class.
  3) Iterate through the list while applying the filter to each item.

  my_list = [Foo(name="steve", age=45, date=datetime.date(2011, 4, 4)),
             Foo(name="jen", age=45, date=datetime.date(2010, 3, 3)),
             Foo(name="kate", age=29, date=datetime.date(2010, 3, 3)),
             Foo(name="jack", age=33, date=datetime.date(2009, 1, 1))]

  If you want to filter the above list by exculding all objects with name
  of "steve" and you only want to include ages 29 or 45, you might use this:

    result = []
    for item in my_list:
      if item.Filter(name__ne="steve", age=(29, 45)).IsValid():
        result.append(item)

  You can also use the 'OR' string as the first argument to the Filter method
  to flip the Filter method from 'AND' mode to 'OR' mode (AND is on by default):

    item.Filter('OR', date__gte=datetime.date(2011, 1, 1), age__lt=45).IsValid()

  Chaining is possible as well:

      item.Filter(name__ne="steve").Filter(age=(29, 45)).IsValid()

  The code above will filter for dates >= 2011-01-01 OR age < 45. You must add
  two underscores after the class attribute/parameter name, followed by the
  comparison type, like this:

    name --> name__gt, name__gte, name__ne, name__lte, name__lt, etc.
    age --> age__gt, age__gte, age__ne, age__lte, age__lt, etc.
    ...
"""

__author__ = 'jbenjamin@google.com (Jason Debolt)'


class Error(Exception):
  """Base exception for EasyFilter class."""


class InvalidEasyFilterKeywordArgument(Error):
  """If a given keyword argument is invalid and cannot be parsed."""


class EasyFilter(object):
  """Handles the querying of a single object with a query like syntax.

  Attributes:
    __mode: String query mode, i.e. 'AND', 'OR'.
    __or_mode: Bool indicating if 'OR' mode is set.
    __other_value: Keyword value other value provided by caller.
    __this_value: Value/Field of comparison object to compare against.
    __lt: String name of less than keyword extension, i.e. '__lt'.
    __lte: String name of less than or equal to keyword extension, i.e. '__lte'.
    __ne: String name of not equal to keyword extension, i.e. '__ne'.
    __gte: String name greater than or equal to keyword extension, i.e. '__gte'.
    __gt: String name greater than keyword extension, i.e. '__gt'.
    _is_valid: Bool value indicating if the object passes all filters/tests.
    __operator_dict: Dictionary of operator string selectors to their methods.
  """

  def __init__(self):
    """Initalizes the EasyFilter class."""
    self.__mode = None
    self.__or_mode = False
    self.__other_value = None
    self.__this_value = None
    self.__lt = '__lt'
    self.__lte = '__lte'
    self.__ne = '__ne'
    self.__gte = '__gte'
    self.__gt = '__gt'
    self._is_valid = True
    self.__operator_dict = {
        self.__lt: self.__CheckLt,
        self.__lte: self.__CheckLte,
        self.__ne: self.__CheckNe,
        self.__gte: self.__CheckGte,
        self.__gt: self.__CheckGt}

  def Filter(self, mode='AND', **kwargs):
    """Filter an object using special keyword arguments for complex querying.

    This method sets the IsValid() method to False for the object if any
    of the tests listed in the keyword args fail, then returns the object. If
    in 'OR' mode, only one of the tests needs to pass to return self, leaving
    the IsValid() to return True.

    Args:
      mode: String query mode, i.e. 'AND', 'OR'.
      kwargs: Dict of class attributes to object values related to an object.

    Returns:
      Self.

    Raises:
      InvalidEasyFilterKeywordArgument: Error if the keyward cannot be parsed.
    """
    # quit testing if object is no longer valid.
    if not self._is_valid:
      return self
    # TODO(jbenjamin): Add ValidateKwarg() method to ensure the kwarg has a
    # valid attrib and operator.
    for kwarg in kwargs:
      operator_method = None
      self.__SetFilterSettings(mode, kwargs, kwarg)
      if kwarg in self.__dict__:
        self.__this_value = self.__dict__.get(kwarg)
        if self.__CheckTuple() or self.__CheckEqual():
          if self.__or_mode:
            return self
          continue
      else:
        for operator in self.__operator_dict:
          if kwarg.endswith(operator):
            kwarg = kwarg.replace(operator, '')
            if kwarg not in self.__dict__:
              raise InvalidEasyFilterKeywordArgument(
                  'EasyFilter cannot parse this keyword argument: %s' % kwarg)
            self.__this_value = self.__dict__.get(kwarg)
            operator_method = self.__operator_dict.get(operator)
            break
        try:
          if operator_method():
            if self.__or_mode:
              return self
            continue
        except TypeError:
          raise InvalidEasyFilterKeywordArgument(
              'EasyFilter cannot parse this keyword argument: %s' % kwarg)
      if self.__or_mode:
        continue
      else:
        self._is_valid = False
        return self
    if self.__or_mode:
      self._is_valid = False
    return self

  def __SetFilterSettings(self, mode, kwargs, keyname):
    """Sets the filter settings.

    Args:
      mode: String query mode, i.e. 'AND', 'OR'.
      kwargs: Dict of class attributes to object values related to an object.
      keyname: String name of object attribute/field, i.e. 'name', 'age', etc.
    """
    self.__mode = mode
    self.__or_mode = self.__mode == 'OR'
    self.__other_value = kwargs.get(keyname)

  def IsValid(self):
    """Determine if the object is valid (passes all filters).

    Returns:
      Bool.
    """
    is_valid = self._is_valid
    self._is_valid = True
    return is_valid

  def __CheckTuple(self):
    """Determine if the kwarg is a tuple and if any tuple items are matches.

    Returns:
      Bool true if an item within tuple kwarg matches the comparison value.
    """
    if isinstance(self.__other_value, tuple):
      for item in self.__other_value:
        if item == self.__this_value:
          return True

  def __CheckEqual(self):
    """Determine if the value is equal to the kwarg value.

    Returns:
      Bool true if object value is equal to the kwarg/other value.
    """
    return self.__this_value == self.__other_value

  def __CheckLt(self):
    """Determine if the value is less than the provided kwarg value.

    Returns:
      Bool true if object value is less than kwarg value.
    """
    return self.__this_value < self.__other_value

  def __CheckLte(self):
    """Determine if the value is less than or equal to the provided kwarg value.

    Returns:
      Bool true if object value is less than or equal to kwarg value.
    """
    return self.__this_value <= self.__other_value

  def __CheckNe(self):
    """Determine if the value is not equal to the provided kwarg value.

    Returns:
      Bool true if object value is not equal to kwarg value.
    """
    return self.__this_value != self.__other_value

  def __CheckGte(self):
    """Determine if the value is greater than or equal to provided kwarg value.

    Returns:
      Bool true if object value is greater than or equal to kwarg value.
    """
    return self.__this_value >= self.__other_value

  def __CheckGt(self):
    """Determine if the value is greater than the provided kwarg value.

    Returns:
      Bool true if object value is less than kwarg value.
    """
    return self.__this_value > self.__other_value
